﻿using System;
using System.Collections.Generic;
using System.Linq;
using Gumini.JulaDB.Data;
using Gumini.JulaDB.Enums;
using Gumini.JulaDB.Expressions;
using Gumini.JulaDB.Parser;
using Gumini.JulaDB.Provider;
using Irony.Parsing;

namespace Gumini.JulaDB.Planner
{
    /// <summary>
    /// Responsible for constructing a query plan from the SQL parse tree.
    /// </summary>
    internal class QueryPlanner
    {
        private readonly SqlGrammar _grammar;
        private readonly Database _database;
        private readonly ExpressionBuilder _expressionBuilder;

        public QueryPlanner(SqlGrammar grammar, Database database, ExpressionBuilder expressionBuilder)
        {
            _grammar = grammar;
            _database = database;
            _expressionBuilder = expressionBuilder;
        }

        public Expression GetWhereExpression(ParseTreeNode node)
        {
            var whereNode = node.GetSubNode(_grammar.whereClauseOpt);
            if (whereNode.ChildNodes.Any())
            {
                var expressionNode = whereNode.ChildNodes[1];
                return _expressionBuilder.CreateExpression(expressionNode);
            }
            else
                return null;
        }

        /// <summary>
        /// Creates a plan subtree that joins all of the input tables.
        /// This is initial naive implementation that does a brute force cartesian join on all tables.
        /// </summary>
        private PlanNode BuildJoinTree(List<TableNode> leafNodes)
        {
            PlanNode result = leafNodes.First();
            foreach (var node in leafNodes.Skip(1))
                result = new CrossJoinNode(result, node);
            return result;
        }

        private TableNode BuildTableNode(ParseTreeNode nameNode)
        {
            if (nameNode.Term != _grammar.aliasedId)
                throw new ArgumentException("Expected AliasedId");

            string tableName = nameNode.ChildNodes.First().GetName(_grammar);
            string tableAlias = nameNode.ChildNodes.Count > 1 ? nameNode.ChildNodes[1].GetName(_grammar) : null;

            return new TableNode(
                table: _database.GetTable(tableName),
                alias: tableAlias);
        }

        private TableNode GetJoinTable(ParseTreeNode joinNode)
        {
            if (joinNode.ChildNodes.Count == 1) // this is an implcit (comma) join
            {
                return BuildTableNode(joinNode.ChildNodes.Single());
            }
            else
                throw new JulaDbException(ErrorCode.UnsupportedSyntax, "Only implcit (comma) JOIN syntax is supported.");
        }

        public QueryPlan BuildQueryPlan(ParseTreeNode selectNode)
        {
            // throw an exception on unsupported syntax
            if (selectNode.GetSubNode(_grammar.havingClauseOpt).ChildNodes.Any())
                throw new JulaDbException(ErrorCode.UnsupportedSyntax, "HAVING is not supported.");
            if (selectNode.GetSubNode(_grammar.intoClauseOpt).ChildNodes.Any())
                throw new JulaDbException(ErrorCode.UnsupportedSyntax, "INTO is not supported.");

            // FROM
            var fromNode = selectNode.GetSubNode(_grammar.fromClauseOpt);
            if (!fromNode.ChildNodes.Any()) // check if the from clause was specified
                throw new JulaDbException(ErrorCode.UnsupportedSyntax, "FROM statement must be specified.");
            var firstTableNode = fromNode.GetSubNode(_grammar.aliasedId);
            
            var sourceNodes = new List<TableNode> { BuildTableNode(firstTableNode) };
            var joinOptNode = fromNode.GetSubNode(_grammar.joinSequenceOpt);
            foreach(var joinNode in joinOptNode.ChildNodes)
                sourceNodes.Add(GetJoinTable(joinNode));

            // getting the source tables and construcing leaf nodes
            PlanNode rootNode = BuildJoinTree(sourceNodes);

            // WHERE
            var whereExpression = GetWhereExpression(selectNode);
            if (whereExpression != null)
                rootNode = new FilterNode(rootNode, whereExpression);

            // ORDER BY node
            var orderList = selectNode.GetSubNode(_grammar.orderClauseOpt).GetSubNode(_grammar.orderList);
            if (orderList != null)
            {
                var orderItems = new List<OrderItem>();
                foreach (var n in orderList.ChildNodes)
                {
                    var columnName = n.GetSubNode(_grammar.Id).GetName(_grammar, allowCompositeNames: true);
                    var direction = SortDirection.Ascending;
                    var orderDir = n.GetSubNode(_grammar.orderDirOpt);
                    if (orderDir != null && orderDir.ChildNodes.Any() && orderDir.ChildNodes[0].Token.Text.ToUpperInvariant() == "DESC")
                        direction = SortDirection.Descending;
                    orderItems.Add(new OrderItem(columnName, direction));
                }
                rootNode = new OrderByNode(rootNode, orderItems);
            }

            // '*' (asterisk) or a list of column names
            var selNode = selectNode.GetSubNode(_grammar.selList);
            if (selNode.ChildNodes.First().Term == _grammar.columnItemList)
            { // check if column names are specified
                var columnItems = selNode.ChildNodes.First().ChildNodes;

                // getting the GROUP BY column names
                var groupByList = selectNode.
                    GetSubNode(_grammar.groupClauseOpt).
                    GetSubNode(_grammar.exprList);

                var columnExpressionItems = new List<ColumnExpressionItem>();
                foreach (var expression in columnItems)
                {
                    string aliasName = null;
                    // alias can be specified optionally
                    if (expression.ChildNodes.Count > 1)
                    {
                        var aliasId = expression.ChildNodes[1];
                        aliasName = aliasId.GetName(_grammar);
                    }

                    var columnExpression = _expressionBuilder.CreateExpression(expression.ChildNodes[0]);
                    columnExpressionItems.Add(new ColumnExpressionItem(
                        expression: columnExpression,
                        alias: aliasName));
                }

                var groupByExpressions = new List<Expression>();
                if (groupByList != null)
                {
                    groupByExpressions = groupByList.
                        ChildNodes.
                        Select(n => _expressionBuilder.CreateExpression(n)).
                        ToList();
                }

                rootNode = new ProjectionNode(rootNode, columnExpressionItems, groupByExpressions);
            }

            // empty, ALL or DISTINCT
            var selRestr = selectNode.GetSubNode(_grammar.selRestrOpt);
            bool selectDistinct = selRestr.ChildNodes.Any() && selRestr.ChildNodes[0].Token.Text.ToUpperInvariant() == "DISTINCT";
            if (selectDistinct)
                rootNode = new DistinctNode(rootNode);

            return new QueryPlan(rootNode);
        }
    }
}
